<?php

class HOTP {
	private $secret = null;
	private $counter = 0;
	
	public function __construct($secret = null, $counter = 0) {
		$this->secret = $secret;
		$this->counter = $counter;
	}
	
	public function setSecret($secret, $format = null) {
		$this->secret = self::_convertSecret($secret, $format);
	}
	
	public function gen($counter = null) {
		if ($counter === null) {
			if ($this->counter === null) {
				$this->counter = 0;
			}
			$counter = $this->counter;
		}
		return self::_gen($this->secret, $counter);
	}
	
	public function verify($token, $counter = null) {
		if ($counter === null) {
			if ($this->counter === null) {
				$this->counter = 0;
			}
			$counter = $this->counter;
			if ($token === $this->gen(counter)) {
				$this->counter += 1;
				return true;
			}
			return false;
		}
		return $token === $this->gen($counter);
	}
	
	private static function _gen($secret, $counter) {
		$data = pack('NN', $counter / 4294967296, $counter & 0xFFFFFFFF);
		$hash = hash_hmac('sha1', $data, $secret, true);
		$offset = ord($hash[19]) &0xf;
		$num = (ord($hash[$offset]) & 0x7F) << 24 |
			(ord($hash[$offset + 1]) & 0xFF) << 16 |
			(ord($hash[$offset + 2]) & 0xFF) << 8  |
			(ord($hash[$offset + 3]) & 0xFF);
		$num = $num % 1000000;
		return sprintf("%06d", $num);
	}
	
	private static function _convertSecret($secret, $format) {
		if (!is_string($secret))
			throw Exception('参数错误');
		if ('hex' === $format)
			return hex2bin($secret);
		else if ('base64' === $format)
			return base64_decode($secret);
		else if ('string' === $format || null === $format)
			return $secret;
		else
			throw Exception('参数错误');
	}
}

class TOTP {
	private $hotp;
	private $window = 3;
	private $interval = 30;
	
	public function __construct($secret = null, $window = 3, $interval = 30) {
		$this->hotp = new HOTP($secret, 0);
		$this->window = $window;
		$this->interval = $interval;
	}
	
	public function setSecret($secret, $format = null) {
		$this->hotp->setSecret($secret, $format);
	}
	
	public function gen($timestamp = null) {
		$timestamp = empty($timestamp) ? (time() * 1000) : $timestamp;
		$counter = floor($timestamp / 1000 / $this->interval);
		return $this->hotp->gen($counter);
	}
	
	public function gen2($timestamp = null) {
		$timestamp = empty($timestamp) ? (time() * 1000) : $timestamp;
		$timestamp_second = $timestamp / 1000;
		$counter = floor($timestamp_second / $this->interval);
		$token = $this->hotp->gen($counter);
		$step = $timestamp_second % $this->interval;
		$refresh_time = $counter * $this->interval;
		return array("token" => $token,
			"loop_length" => $this->interval,
			"loop_step" => $step,
			"refresh_interval" => $this->interval * 1000,
			"refresh_time" => $refresh_time * 1000,
			"timestamp" => $timestamp
			);
	}
	
	public function verify($token, $timestamp = null) {
		$$timestamp = empty($timestamp) ? (time() * 1000) : $timestamp;
		$counter = floor($timestamp / 1000 / $this->interval);
		
		for ($i = 0, $w = 0; ; $i++) {
			if ($token === $this->hotp->gen($counter - $i))
				return true;
			if (++$w >= $this->window)
				break;
			if ($i > 0) {
				if ($token === $this->hotp->gen($counter + $i))
					return true;
				if (++$w >= $this->window)
					break;
			}
		}
		return false;
	}
}

/**

function test() {
	echo "\n<pre>\n";
	
	$p1 = new HOTP("123456", 0);
	echo $p1->gen() . "\n"; # 186818
	echo $p1->gen(1234) . "\n"; # 263197
	echo ($p1->verify('263197', 1234) ? "TRUE" : "FALSE") . "\n";

	$p2 = new TOTP("123456", 3, 30);
	echo $p2->gen(1614054214456) . "\n"; # 330925
	echo ($p2->verify('330925', 1614054214456) ? "TRUE" : "FALSE") . "\n";

	$p3 = new TOTP('helloworld', 3, 30);
	$p3->setSecret('helloworld', 'string');
	$p3->setSecret('68656c6c6f776f726c64', 'hex');
	$p3->setSecret('aGVsbG93b3JsZA==', 'base64');
	echo $p3->gen(1614054214456) . "\n"; # 759440
	echo ($p3->verify('759440', 1614054214456) ? "TRUE" : "FALSE") . "\n";

	echo "\n</pre>\n";
}
test();
**/
